สำรวจเทคนิคขั้นสูงสำหรับการดึงข้อมูลแบบขนานใน React โดยใช้ Suspense ปรับปรุงประสิทธิภาพของแอปพลิเคชันและประสบการณ์ผู้ใช้ เรียนรู้กลยุทธ์ในการประสานงานการทำงานแบบอะซิงโครนัสหลายรายการและจัดการสถานะการโหลดอย่างมีประสิทธิภาพ
การประสานงาน React Suspense: การควบคุมการดึงข้อมูลแบบขนาน
React Suspense ได้ปฏิวัติวิธีการจัดการการทำงานแบบอะซิงโครนัสของเรา โดยเฉพาะการดึงข้อมูล ซึ่งช่วยให้คอมโพเนนต์ "ระงับ" การเรนเดอร์ขณะรอการโหลดข้อมูล โดยให้วิธีการประกาศในการจัดการสถานะการโหลด อย่างไรก็ตาม การห่อการดึงข้อมูลแต่ละรายการด้วย Suspense เพียงอย่างเดียวนั้น อาจนำไปสู่เอฟเฟกต์น้ำตก ซึ่งการดึงข้อมูลรายการหนึ่งเสร็จสิ้นก่อนที่รายการถัดไปจะเริ่มต้น ซึ่งส่งผลเสียต่อประสิทธิภาพ โพสต์บล็อกนี้เจาะลึกกลยุทธ์ขั้นสูงสำหรับการประสานงานการดึงข้อมูลหลายรายการแบบขนานโดยใช้ Suspense ซึ่งช่วยเพิ่มการตอบสนองของแอปพลิเคชันและปรับปรุงประสบการณ์ผู้ใช้สำหรับผู้ชมทั่วโลก
การทำความเข้าใจปัญหา Waterfall ในการดึงข้อมูล
ลองนึกภาพสถานการณ์ที่คุณต้องแสดงโปรไฟล์ผู้ใช้พร้อมชื่อ อวาตาร์ และกิจกรรมล่าสุด หากคุณดึงข้อมูลแต่ละส่วนตามลำดับ ผู้ใช้จะเห็นตัวหมุนโหลดสำหรับชื่อ จากนั้นสำหรับอวาตาร์ และสุดท้ายสำหรับฟีดกิจกรรม รูปแบบการโหลดตามลำดับนี้สร้างเอฟเฟกต์น้ำตก ทำให้การแสดงโปรไฟล์ทั้งหมดล่าช้าและทำให้ผู้ใช้หงุดหงิด สำหรับผู้ใช้ต่างประเทศที่มีความเร็วเครือข่ายแตกต่างกัน ความล่าช้านี้อาจเด่นชัดยิ่งขึ้น
พิจารณาโค้ดตัวอย่างแบบง่ายนี้:
function UserProfile() {
const name = useName(); // ดึงชื่อผู้ใช้
const avatar = useAvatar(name); // ดึงอวาตาร์ตามชื่อ
const activity = useActivity(name); // ดึงกิจกรรมตามชื่อ
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="User Avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
ในตัวอย่างนี้ useAvatar และ useActivity ขึ้นอยู่กับผลลัพธ์ของ useName ซึ่งสร้างน้ำตกที่ชัดเจน – useAvatar และ useActivity ไม่สามารถเริ่มดึงข้อมูลได้จนกว่า useName จะเสร็จสมบูรณ์ นี่เป็นวิธีการที่ไม่ถูกต้องและเป็นคอขวดด้านประสิทธิภาพทั่วไป
กลยุทธ์สำหรับการดึงข้อมูลแบบขนานด้วย Suspense
กุญแจสำคัญในการเพิ่มประสิทธิภาพการดึงข้อมูลด้วย Suspense คือการเริ่มต้นคำขอข้อมูลทั้งหมดพร้อมกัน นี่คือกลยุทธ์หลายประการที่คุณสามารถนำไปใช้ได้:
1. การโหลดข้อมูลล่วงหน้าด้วย `React.preload` และทรัพยากร
หนึ่งในเทคนิคที่มีประสิทธิภาพที่สุดคือการโหลดข้อมูลล่วงหน้าก่อนที่คอมโพเนนต์จะแสดงผล ซึ่งเกี่ยวข้องกับการสร้าง "ทรัพยากร" (วัตถุที่รวบรวมสัญญาการดึงข้อมูล) และการดึงข้อมูลล่วงหน้า `React.preload` ช่วยในเรื่องนี้ เมื่อถึงเวลาที่คอมโพเนนต์ต้องการข้อมูล ข้อมูลนั้นพร้อมใช้งานอยู่แล้ว ซึ่งช่วยลดสถานะการโหลดลง
พิจารณาแหล่งข้อมูลสำหรับการดึงข้อมูลผลิตภัณฑ์:
const createProductResource = (productId) => {
let promise;
let product;
let error;
const suspender = new Promise((resolve, reject) => {
promise = fetch(`/api/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
product = data;
resolve();
})
.catch(e => {
error = e;
reject(e);
});
});
return {
read() {
if (error) {
throw error;
}
if (product) {
return product;
}
throw suspender;
},
};
};
// การใช้งาน:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
ตอนนี้ คุณสามารถโหลดทรัพยากรนี้ล่วงหน้าก่อนที่จะแสดงคอมโพเนนต์ ProductDetails ตัวอย่างเช่น ระหว่างการเปลี่ยนเส้นทาง หรือเมื่อวางเมาส์เหนือ
React.preload(productResource);
ซึ่งช่วยให้มั่นใจได้ว่าข้อมูลนั้นพร้อมใช้งานเมื่อคอมโพเนนต์ ProductDetails ต้องการข้อมูล ซึ่งช่วยลดหรือกำจัดสถานะการโหลด
2. การใช้ `Promise.all` สำหรับการดึงข้อมูลพร้อมกัน
อีกวิธีที่ง่ายและมีประสิทธิภาพคือการใช้ Promise.all เพื่อเริ่มต้นการดึงข้อมูลทั้งหมดพร้อมกันภายในขอบเขต Suspense เดียวกัน ซึ่งใช้งานได้ดีเมื่อทราบการขึ้นต่อกันของข้อมูลล่วงหน้า
มาดูตัวอย่างโปรไฟล์ผู้ใช้อีกครั้ง แทนที่จะดึงข้อมูลตามลำดับ เราสามารถดึงชื่อ อวาตาร์ และฟีดกิจกรรมพร้อมกัน:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// จำลองการเรียก API
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// จำลองการเรียก API
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// จำลองการเรียก API
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
function Name() {
const name = useSuspense(fetchName());
return <h2>{name}</h2>;
}
function Avatar({ name }) {
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity({ name }) {
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const name = useSuspense(fetchName());
return (
<div>
<Suspense fallback=<div>กำลังโหลดอวาตาร์...</div>>
<Avatar name={name} />
</Suspense>
<Suspense fallback=<div>กำลังโหลดกิจกรรม...</div>>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
อย่างไรก็ตาม หากแต่ละรายการของ `Avatar` และ `Activity` ยังขึ้นอยู่กับ `fetchName` แต่แสดงผลภายในขอบเขต Suspense แยกกัน คุณสามารถยกคำสัญญา `fetchName` ไปยังพาเรนต์และให้ผ่านบริบท React ได้
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// จำลองการเรียก API
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// จำลองการเรียก API
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// จำลองการเรียก API
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
const NamePromiseContext = createContext(null);
function Avatar() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const namePromise = fetchName();
return (
<NamePromiseContext.Provider value={namePromise}>
<Suspense fallback=<div>กำลังโหลดอวาตาร์...</div>>
<Avatar />
</Suspense>
<Suspense fallback=<div>กำลังโหลดกิจกรรม...</div>>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. การใช้ Hook แบบกำหนดเองเพื่อจัดการการดึงข้อมูลแบบขนาน
สำหรับสถานการณ์ที่ซับซ้อนมากขึ้นด้วยการพึ่งพาข้อมูลตามเงื่อนไข คุณสามารถสร้าง hook แบบกำหนดเองเพื่อจัดการการดึงข้อมูลแบบขนานและส่งคืนทรัพยากรที่ Suspense สามารถใช้ได้
import { useState, useEffect, useRef } from 'react';
function useParallelData(fetchFunctions) {
const [resource, setResource] = useState(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
const promises = fetchFunctions.map(fn => fn());
const suspender = Promise.all(promises).then(
(results) => {
if (mounted.current) {
setResource({ status: 'success', value: results });
}
},
(error) => {
if (mounted.current) {
setResource({ status: 'error', value: error });
}
}
);
setResource({
status: 'pending',
value: suspender,
});
return () => {
mounted.current = false;
};
}, [fetchFunctions]);
const read = () => {
if (!resource) {
throw new Error('Resource not yet initialized');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// ตัวอย่างการใช้งาน:
async function fetchUserData(userId) {
// จำลองการเรียก API
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'User ' + userId };
}
async function fetchUserPosts(userId) {
// จำลองการเรียก API
await new Promise(resolve => setTimeout(resolve, 500));
return [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }];
}
function UserProfile({ userId }) {
const { read } = useParallelData([
() => fetchUserData(userId),
() => fetchUserPosts(userId),
]);
const [userData, userPosts] = read();
return (
<div>
<h2>{userData.name}</h2>
<ul>
{userPosts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback=<div>กำลังโหลดข้อมูลผู้ใช้...</div>>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
แนวทางนี้สรุปความซับซ้อนในการจัดการสัญญาและการโหลดสถานะภายใน hook ทำให้โค้ดคอมโพเนนต์สะอาดขึ้นและมุ่งเน้นไปที่การแสดงผลข้อมูลมากขึ้น
4. การไฮเดรชั่นแบบเลือกด้วยการแสดงผลเซิร์ฟเวอร์แบบสตรีมมิง
สำหรับแอปพลิเคชันที่แสดงผลบนเซิร์ฟเวอร์ React 18 แนะนำการไฮเดรชั่นแบบเลือกด้วยการแสดงผลเซิร์ฟเวอร์แบบสตรีมมิง ซึ่งช่วยให้คุณสามารถส่ง HTML ไปยังไคลเอ็นต์ในรูปแบบกลุ่มเมื่อพร้อมใช้งานบนเซิร์ฟเวอร์ คุณสามารถห่อคอมโพเนนต์ที่โหลดช้าด้วยขอบเขต <Suspense> ซึ่งช่วยให้ส่วนที่เหลือของเพจโต้ตอบได้ในขณะที่คอมโพเนนต์ที่ช้ายังคงโหลดบนเซิร์ฟเวอร์ ซึ่งช่วยเพิ่มประสิทธิภาพที่รับรู้ได้อย่างมาก โดยเฉพาะอย่างยิ่งสำหรับผู้ใช้ที่มีการเชื่อมต่อเครือข่ายหรืออุปกรณ์ที่ช้า
พิจารณาสถานการณ์ที่เว็บไซต์ข่าวต้องแสดงบทความจากภูมิภาคต่างๆ ทั่วโลก (เช่น เอเชีย ยุโรป อเมริกา) แหล่งข้อมูลบางแหล่งอาจช้ากว่าแหล่งข้อมูลอื่นๆ การไฮเดรชั่นแบบเลือกช่วยให้แสดงบทความจากภูมิภาคที่เร็วก่อน ในขณะที่บทความจากภูมิภาคที่ช้ากว่ายังคงโหลดอยู่ ซึ่งช่วยป้องกันไม่ให้ทั้งเพจถูกบล็อก
การจัดการข้อผิดพลาดและสถานะการโหลด
ในขณะที่ Suspense ทำให้การจัดการสถานะการโหลดง่ายขึ้น การจัดการข้อผิดพลาดยังคงมีความสำคัญ ขอบเขตข้อผิดพลาด (โดยใช้เมธอดวงจรชีวิต componentDidCatch หรือ hook useErrorBoundary จากไลบรารีต่างๆ เช่น `react-error-boundary`) ช่วยให้คุณสามารถจัดการข้อผิดพลาดที่เกิดขึ้นระหว่างการดึงข้อมูลหรือการแสดงผลได้อย่างสง่างาม ขอบเขตข้อผิดพลาดเหล่านี้ควรถูกวางไว้อย่างมีกลยุทธ์เพื่อจับข้อผิดพลาดภายในขอบเขต Suspense เฉพาะ เพื่อป้องกันไม่ให้แอปพลิเคชันทั้งหมดขัดข้อง
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... ดึงข้อมูลที่อาจเกิดข้อผิดพลาด
}
function App() {
return (
<ErrorBoundary fallback={<div>มีบางอย่างผิดพลาด!</div>}>
<Suspense fallback={<div>กำลังโหลด...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
อย่าลืมจัดเตรียม UI สำรองที่ให้ข้อมูลและเป็นมิตรกับผู้ใช้สำหรับทั้งสถานะการโหลดและข้อผิดพลาด ซึ่งมีความสำคัญอย่างยิ่งสำหรับผู้ใช้ต่างประเทศที่อาจประสบกับความเร็วเครือข่ายที่ช้าลงหรือการหยุดให้บริการระดับภูมิภาค
แนวทางปฏิบัติที่ดีที่สุดสำหรับการเพิ่มประสิทธิภาพการดึงข้อมูลด้วย Suspense
- ระบุและจัดลำดับความสำคัญของข้อมูลที่สำคัญ: กำหนดว่าข้อมูลใดจำเป็นสำหรับการแสดงผลเริ่มต้นของแอปพลิเคชันของคุณ และจัดลำดับความสำคัญในการดึงข้อมูลนั้นก่อน
- โหลดข้อมูลล่วงหน้าเมื่อเป็นไปได้: ใช้ `React.preload` และแหล่งข้อมูลเพื่อโหลดข้อมูลล่วงหน้าก่อนที่คอมโพเนนต์จะต้องการ ซึ่งช่วยลดสถานะการโหลด
- ดึงข้อมูลพร้อมกัน: ใช้ประโยชน์จาก `Promise.all` หรือ hook แบบกำหนดเองเพื่อเริ่มต้นการดึงข้อมูลหลายรายการแบบขนาน
- เพิ่มประสิทธิภาพจุดสิ้นสุด API: ตรวจสอบให้แน่ใจว่าจุดสิ้นสุด API ของคุณได้รับการปรับให้เหมาะสมสำหรับประสิทธิภาพ ซึ่งช่วยลดเวลาแฝงและขนาดเพย์โหลด พิจารณาใช้เทคนิคต่างๆ เช่น GraphQL เพื่อดึงข้อมูลที่คุณต้องการเท่านั้น
- ใช้การแคช: แคชข้อมูลที่เข้าถึงบ่อยครั้งเพื่อลดจำนวนคำขอ API พิจารณาใช้ไลบรารีต่างๆ เช่น `swr` หรือ `react-query` สำหรับความสามารถในการแคชที่แข็งแกร่ง
- ใช้ Code Splitting: แบ่งแอปพลิเคชันของคุณออกเป็นกลุ่มที่เล็กลงเพื่อลดเวลาในการโหลดเริ่มต้น ผสมผสานการแยกโค้ดกับ Suspense เพื่อโหลดและแสดงผลส่วนต่างๆ ของแอปพลิเคชันของคุณอย่างต่อเนื่อง
- ตรวจสอบประสิทธิภาพ: ตรวจสอบประสิทธิภาพของแอปพลิเคชันของคุณเป็นประจำโดยใช้เครื่องมือต่างๆ เช่น Lighthouse หรือ WebPageTest เพื่อระบุและแก้ไขคอขวดด้านประสิทธิภาพ
- จัดการข้อผิดพลาดอย่างสง่างาม: ใช้ขอบเขตข้อผิดพลาดเพื่อจับข้อผิดพลาดระหว่างการดึงข้อมูลและการแสดงผล โดยให้ข้อความแสดงข้อผิดพลาดที่ให้ข้อมูลแก่ผู้ใช้
- พิจารณาการแสดงผลฝั่งเซิร์ฟเวอร์ (SSR): ด้วยเหตุผลด้าน SEO และประสิทธิภาพ พิจารณาใช้ SSR พร้อมสตรีมมิงและการไฮเดรชั่นแบบเลือกเพื่อมอบประสบการณ์เริ่มต้นที่รวดเร็วยิ่งขึ้น
บทสรุป
React Suspense เมื่อรวมกับกลยุทธ์สำหรับการดึงข้อมูลแบบขนาน จะมอบชุดเครื่องมืออันทรงพลังสำหรับการสร้างเว็บแอปพลิเคชันที่ตอบสนองและมีประสิทธิภาพ ด้วยการทำความเข้าใจปัญหาเกี่ยวกับน้ำตกและใช้เทคนิคต่างๆ เช่น การโหลดล่วงหน้า การดึงข้อมูลพร้อมกันด้วย Promise.all และ hook แบบกำหนดเอง คุณสามารถปรับปรุงประสบการณ์การใช้งานของผู้ใช้ได้อย่างมาก อย่าลืมจัดการข้อผิดพลาดอย่างสง่างามและตรวจสอบประสิทธิภาพเพื่อให้แน่ใจว่าแอปพลิเคชันของคุณยังคงเหมาะสมที่สุดสำหรับผู้ใช้ทั่วโลก ในขณะที่ React ยังคงพัฒนาอย่างต่อเนื่อง การสำรวจคุณสมบัติใหม่ๆ เช่น การไฮเดรชั่นแบบเลือกด้วยการแสดงผลเซิร์ฟเวอร์แบบสตรีมมิง จะช่วยเพิ่มความสามารถของคุณในการมอบประสบการณ์การใช้งานที่ยอดเยี่ยม ไม่ว่าจะอยู่ที่ใดหรือสภาพเครือข่ายก็ตาม ด้วยการนำเทคนิคเหล่านี้ไปใช้ คุณสามารถสร้างแอปพลิเคชันที่ไม่เพียงแต่ใช้งานได้จริงเท่านั้น แต่ยังสร้างความสุขในการใช้งานสำหรับผู้ชมทั่วโลกของคุณอีกด้วย
โพสต์บล็อกนี้มีวัตถุประสงค์เพื่อให้ภาพรวมที่ครอบคลุมของกลยุทธ์การดึงข้อมูลแบบขนานด้วย React Suspense เราหวังว่าคุณจะพบว่าข้อมูลนี้เป็นประโยชน์และเป็นประโยชน์ เราขอแนะนำให้คุณทดลองใช้เทคนิคเหล่านี้ในโครงการของคุณเองและแบ่งปันผลการค้นพบของคุณกับชุมชน